53. Pandas模块的统计计算

本章概要

  • 学习材料:中性净值、收益率与行业分组教学示例。
  • 本章任务:运行 lst-drawdownlst-rolling-statslst-correlation,计算回撤、滚动统计和关系矩阵。
  • 完成后你将得到:最大回撤表、滚动窗口尾值、最大带符号相关对与绝对值最强相关对。
  • 自我检查:手算峰谷回撤,排除对角/重复对,并分别核对 idxmax()abs().idxmax();窗口不足或两种口径混写时先检查原因。
  • 拓展练习:把同一组统计方法应用到本地真实净值,并说明数据来源。

本章学习目标

  • 掌握 Pandas 描述性统计方法(均值、中位数、标准差等)
  • 理解偏度与峰度的金融含义
  • 学会使用 累积统计量 进行回撤分析
  • 掌握 滚动窗口(rolling)与扩展窗口(expanding)计算
  • 运用 groupby 进行分组统计
  • 理解协方差与相关系数在投资组合中的作用

统计计算是数据分析的核心

统计学是从数据中提取知识的科学。在金融领域,统计计算帮助我们:

  • 量化风险:计算波动率、VaR(在险价值)
  • 评估收益:计算收益率、夏普比率
  • 发现规律:识别相关性、趋势、周期性
  • 做出决策:基于统计显著性的投资决策

Pandas 提供了丰富的统计计算方法,结合 NumPy 的向量化运算,可以高效处理大规模金融数据。

描述性统计:集中趋势

给定数据集 \(X = \{x_1, x_2, \ldots, x_n\}\)

统计量 含义 公式
均值 (Mean) 数据的”重心” \(\bar{x} = \frac{1}{n}\sum_{i=1}^n x_i\)
中位数 (Median) 排序后中间位置的值 将数据分为两半
众数 (Mode) 出现频率最高的值 最常见的观测值

描述性统计:离散程度与分布形状

统计量 含义 公式
样本方差 (Variance) 偏离均值的平方距离 \(s^2 = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})^2\)
样本标准差 (Std Dev) 样本方差的平方根 \(s = \sqrt{s^2}\)
极差 (Range) 最大值与最小值之差 \(\max(X) - \min(X)\)

分布形状

  • 偏度 (Skewness):衡量分布的对称性
  • 峰度 (Kurtosis):衡量分布的尖峰/厚尾程度

Pandas 口径Series.var()Series.std() 默认 ddof=1;总体统计需显式设置 ddof=0

运行前预测|平台任务解答代码

  • 输入预测:运行前先写出 value_QDIIvalue_QDII_diffvalue_QDII_pctchangelvalue_QDII_pctchange2 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到value_QDII_pctchange2 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务解答代码”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务解答代码

展开完整代码(投影默认折叠)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#任务一
import pandas as pd
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
value_QDII = value_QDII.dropna() #删除缺失值所在行
(value_QDII/value_QDII.iloc[0]).plot(figsize=(8,6),grid=True) #将基金净值按首个交易日进行归一处理并可视化
plt.savefig("1.png")  # 保存图形至文件

#任务二
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
print(value_QDII.max())  #找出每只基金净值的最大值
print(value_QDII.min())  #找出每只基金净值的最小值
print(value_QDII.idxmax()) #最大值所在的索引值
print(value_QDII.idxmin()) #最小值所在的索引值


#任务三
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d')  # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True)  # 将日期列设为value_QDII数据框的索引
value_QDII_diff = value_QDII.diff()  # 计算基金每日净值的变动金额
print(value_QDII_diff.head())  #查看前五行数据
print(value_QDII_diff.tail())  #查看后五行数据

#任务四
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx") 
value_QDII_pctchangel = value_QDII.pct_change() #直接使用函数pct_change计算基金每日净值百分比变动

value_QDII_pctchangel.head()  # 查看value_QDII_pctchangel前5行数据
value_QDII_pctchangel.tail()  # 查看value_QDII_pctchangel后5行数据
value_QDII_diff = value_QDII.diff()  # 计算差分值
value_QDII_pctchange2 = value_QDII_diff/value_QDII.shift(1) #运用任务三的结果计算基金每日净值百分比变动
print(value_QDII_pctchange2.head())  # 输出前几行数据
print(value_QDII_pctchange2.tail())  # 输出最后几行数据

任务复盘|平台任务解答代码

运行后核对:核对 value_QDIIvalue_QDII_diffvalue_QDII_pctchangelvalue_QDII_pctchange2 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

本地执行更正(平台原码保持不变):任务四应先把“日期”转为日期类型并设为索引,再用 select_dtypes(include='number') 选择净值数值列,最后执行 pct_change();否则日期列可能报类型错误,或把日期编码误当连续数值计算收益率。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

基础统计量计算(课堂模拟)

数据声明:以下随机序列仅用于演示 Pandas 方法,不代表任何真实公司、基金或可交易结论。

Listing 1: 计算基础描述性统计量
展开基础统计代码
import pandas as pd
import numpy as np

# 创建课堂模拟收益率数据
np.random.seed(42)  # 固定随机种子以保证每次课堂运行结果一致
returns_data = {  # 定义四项匿名模拟资产的收益率参数
    '模拟资产A': np.random.normal(0.001, 0.02, 100),  # 生成模拟资产A的日收益率
    '模拟资产B': np.random.normal(0.0008, 0.025, 100),  # 生成模拟资产B的日收益率
    '模拟资产C': np.random.normal(0.0005, 0.015, 100),  # 生成模拟资产C的日收益率
    '模拟资产D': np.random.normal(0.0006, 0.018, 100)  # 生成模拟资产D的日收益率
}

df_returns = pd.DataFrame(returns_data)  # 组装课堂模拟收益率数据框

print('课堂模拟收益率(前5行):')  # 标明可见输出不是实证数据
print(df_returns.head())  # 输出模拟样本用于核验后续统计
课堂模拟收益率(前5行):
      模拟资产A     模拟资产B     模拟资产C     模拟资产D
0  0.010934 -0.034584  0.005867 -0.014322
1 -0.001765 -0.009716  0.008912 -0.009483
2  0.013954 -0.007768  0.016746  0.014051
3  0.031461 -0.019257  0.016307  0.011587
4 -0.003683 -0.003232 -0.020165  0.000224

常用统计方法一览

Listing 2: 常用统计量计算
展开统计方法代码
# 计算各项统计量
stats_overview = df_returns.agg(['mean', 'std', 'min', 'max'])  # 将四项核心统计量汇总为紧凑对照表
print('核心统计量对照:')  # 标明可见输出对应均值、波动与取值边界
print(stats_overview.round(4))  # 输出四项模拟资产的任务特异统计结果
核心统计量对照:
       模拟资产A   模拟资产B   模拟资产C   模拟资产D
mean -0.0011  0.0014  0.0015  0.0025
std   0.0182  0.0238  0.0163  0.0159
min  -0.0514 -0.0472 -0.0481 -0.0376
max   0.0380  0.0688  0.0583  0.0400

describe() 一键获取完整统计

Listing 3: describe方法获取完整统计
desc_stats = df_returns.describe()
print(desc_stats)
            模拟资产A       模拟资产B       模拟资产C       模拟资产D
count  100.000000  100.000000  100.000000  100.000000
mean    -0.001077    0.001358    0.001473    0.002523
std      0.018163    0.023842    0.016264    0.015914
min     -0.051395   -0.047169   -0.048119   -0.037630
25%     -0.011018   -0.019342   -0.009332   -0.009606
50%     -0.001539    0.002903    0.001965    0.001503
75%      0.009119    0.014254    0.011067    0.012912
max      0.038046    0.068804    0.058291    0.040016

describe() 返回统计量详解

统计量 含义 公式
count 非缺失值数量 \(n_{\text{valid}}\)
mean 均值 \(\bar{x} = \frac{1}{n}\sum x_i\)
std 标准差 \(\sqrt{\frac{1}{n-1}\sum(x_i-\bar{x})^2}\)
min 最小值 \(\min(X)\)

describe() 统计量详解(续)

统计量 含义 公式
25% 第一四分位数 \(Q_1 = P_{25}\)
50% 中位数 \(Q_2 = P_{50}\)
75% 第三四分位数 \(Q_3 = P_{75}\)
max 最大值 \(\max(X)\)

分位数与自定义分位数计算

Listing 4: 计算自定义分位数
# 计算常用分位数
quantiles = [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]
print('自定义分位数:')
print(df_returns.quantile(quantiles))
自定义分位数:
         模拟资产A     模拟资产B     模拟资产C     模拟资产D
0.10 -0.025732 -0.029081 -0.018236 -0.014619
0.25 -0.011018 -0.019342 -0.009332 -0.009606
0.50 -0.001539  0.002903  0.001965  0.001503
0.75  0.009119  0.014254  0.011067  0.012912
0.90  0.021126  0.030136  0.018103  0.023648
0.95  0.030603  0.047470  0.027249  0.028458
0.99  0.032639  0.062445  0.035451  0.039364

四分位距与异常值检测

Listing 5: 四分位距与异常值检测
展开异常值检测代码
# 四分位距(IQR)
Q1 = df_returns.quantile(0.25)
Q3 = df_returns.quantile(0.75)
IQR = Q3 - Q1
print('四分位距(IQR):')
print(IQR)

# 识别异常值(超出1.5*IQR)
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 检测异常值
outliers = (df_returns < lower_bound) | (df_returns > upper_bound)
print(f'\n异常值数量:')
print(outliers.sum())
四分位距(IQR):
模拟资产A    0.020137
模拟资产B    0.033596
模拟资产C    0.020398
模拟资产D    0.022518
dtype: float64

异常值数量:
模拟资产A    1
模拟资产B    1
模拟资产C    2
模拟资产D    0
dtype: int64

异常值检测的数学原理

箱线图规则 (Boxplot Rule):

  • 正常值\([Q_1 - 1.5 \times IQR, \; Q_3 + 1.5 \times IQR]\)
  • 温和异常值:距离箱体 \(1.5 \sim 3 \times IQR\)
  • 极端异常值:距离箱体 \(> 3 \times IQR\)

Z-Score 方法

\[ \large Z_i = \frac{x_i - \bar{x}}{\sigma} \]

通常认为 \(|Z| > 3\) 为异常值。

偏度:Pandas 的样本估计口径

\(m_k=\frac{1}{n}\sum_{i=1}^n(x_i-\bar{x})^k\)。Pandas Series.skew() 返回经偏差校正的 Fisher–Pearson 样本偏度:

\[ \large G_1=\frac{\sqrt{n(n-1)}}{n-2}\frac{m_3}{m_2^{3/2}},\qquad n>2 \]

  • \(G_1 > 0\):右偏(正偏),右尾较长
  • \(G_1 = 0\):样本偏度为零
  • \(G_1 < 0\):左偏(负偏),左尾较长

峰度:Pandas 的样本估计口径

Pandas Series.kurtosis() 返回经偏差校正的 Fisher 超额峰度。先令 \(g_2=m_4/m_2^2-3\)

\[ \large G_2=\frac{n-1}{(n-2)(n-3)}\left[(n+1)g_2+6\right],\qquad n>3 \]

  • \(G_2 > 0\):第四矩高于正态基准,可能来自尾部或中心质量差异
  • \(G_2 = 0\):样本超额峰度为零
  • \(G_2 < 0\):第四矩低于正态基准,不能单独推出尾部风险较低

这些样本统计量接近 0 只是分布形状线索,不能单独完成正态性检验。

偏度与峰度的金融意义

指标 特征 金融含义
正偏度 右尾较长 右侧极端值候选;不等同于牛市或投资偏好
负偏度 左尾较长 左侧极端值候选;须结合分位数与期间判断风险
高峰度 第四矩较高 需用分布图、分位数和尾部指标继续诊断

一些金融市场样本会呈现负偏度 + 高峰度,但必须用目标期间的真实数据检验,不能由本课堂模拟直接推出。

计算偏度和峰度

Listing 6: 计算偏度和峰度
展开偏度与峰度代码
skewness = df_returns.skew()
kurtosis = df_returns.kurtosis()

distribution_check = []  # 准备用于课堂核验的分布形状摘要

# 解释
for stock in df_returns.columns:
    skew_val = skewness[stock]
    kurt_val = kurtosis[stock]
    if skew_val > 0.5:
        skew_interp = '右偏,极端正收益更多'
    elif skew_val < -0.5:
        skew_interp = '左偏,极端负收益更多'
    else:
        skew_interp = '近似对称'
    if kurt_val > 1:
        kurt_interp = '尖峰厚尾'
    elif kurt_val < -1:
        kurt_interp = '低峰薄尾'
    else:
        kurt_interp = '接近正态'
    distribution_check.append({'模拟资产': stock, '偏度': skew_val, '峰度': kurt_val, '偏度判断': skew_interp, '峰度判断': kurt_interp})  # 保存每项模拟资产的数值与解释
distribution_summary = pd.DataFrame(distribution_check).set_index('模拟资产')  # 合并为四行便于核对结果表
print(distribution_summary.round({'偏度': 3, '峰度': 3}))  # 输出紧凑的偏度、峰度及解释
          偏度     峰度  偏度判断  峰度判断
模拟资产                           
模拟资产A -0.178 -0.101  近似对称  接近正态
模拟资产B  0.387  0.031  近似对称  接近正态
模拟资产C  0.178  1.126  近似对称  尖峰厚尾
模拟资产D  0.200 -0.185  近似对称  接近正态

变化累加与收益复利(课堂模拟)

Listing 7: 计算累积统计量
展开累积统计代码
prices = pd.DataFrame({  # 构造两项匿名模拟资产的价格路径
    '模拟资产A': [100, 102, 101, 104, 103, 106],  # 设置模拟资产A的课堂价格序列
    '模拟资产B': [100, 99, 101, 103, 102, 105]  # 设置模拟资产B的课堂价格序列
})

price_changes = prices.diff()  # 计算可加的逐期价格变化而非直接累加价格水平
cumulative_changes = price_changes.fillna(0).cumsum()  # 累加逐期变化以重构相对期初的价格变动
period_returns = prices.pct_change().fillna(0)  # 计算逐期收益率并将首期基准设为零
wealth_index = (1 + period_returns).cumprod()  # 用复利规则生成以1为起点的财富指数
cumulative_check = pd.DataFrame({'期初价格': prices.iloc[0], '期末价格': prices.iloc[-1], '累计价格变动': cumulative_changes.iloc[-1], '复合财富指数': wealth_index.iloc[-1]})  # 汇总变化累加与收益复利结果
assert np.allclose(cumulative_changes.iloc[-1], prices.iloc[-1] - prices.iloc[0])  # 验证变化累加等于期末减期初
assert np.allclose(wealth_index.iloc[-1], prices.iloc[-1] / prices.iloc[0])  # 验证复合财富等于价格相对期初的比率
print('变化累加与收益复利:')  # 标明两种累积对象不可混用
print(cumulative_check.round(4))  # 输出两项模拟资产的便于核对结果
变化累加与收益复利:
       期初价格  期末价格  累计价格变动  复合财富指数
模拟资产A   100   106     6.0    1.06
模拟资产B   100   105     5.0    1.05

价格历史极值与累计收益率(课堂模拟)

Listing 8: 累积最大值与最小值
展开累积极值代码
# 累计收益率
initial_prices = prices.iloc[0]
cum_returns = (prices / initial_prices - 1) * 100
cumulative_extrema = pd.DataFrame({'历史最高': prices.cummax().iloc[-1], '历史最低': prices.cummin().iloc[-1], '累计收益率(%)': cum_returns.iloc[-1]})  # 汇总期末便于核对的累积极值与收益率
print('价格极值与期末累计收益率:')  # 标明输出用于比较两项模拟资产
print(cumulative_extrema.round(2))  # 输出紧凑的累计统计结果
价格极值与期末累计收益率:
       历史最高  历史最低  累计收益率(%)
模拟资产A   106   100       6.0
模拟资产B   105    99       5.0

累积统计量的金融应用

方法 金融应用场景
cumsum 累加可加的价格变化、现金流或成交量;简单收益率通常不可直接相加
cumprod \((1+r_t)\) 连乘得到复合财富指数
cummax 回撤分析(Drawdown)
cummin 历史最低价监控

最大回撤:衡量投资风险的关键指标

回撤 (Drawdown):从历史最高点到当前点的下降幅度

\[ \large \text{Drawdown}_t = \frac{P_t - \max_{i \leq t} P_i}{\max_{i \leq t} P_i} \]

最大回撤 (Maximum Drawdown, MDD):

\[ \large \text{MDD} = \min_t \text{Drawdown}_t \]

  • MDD = -20% 意味着曾从高点下跌 20%
  • 下跌 20% 需要上涨 25% 才能回本

最大回撤的代码实现(课堂模拟)

Listing 9: 计算最大回撤
展开最大回撤代码
np.random.seed(42)  # 固定随机种子使每次课堂模拟结果一致
simulated_returns = pd.Series(np.random.normal(0.001, 0.02, 100), index=pd.date_range('2024-01-01', periods=100), name='模拟日收益率')  # 生成匿名课堂模拟收益率
compounded_wealth = (1 + simulated_returns).cumprod()  # 按逐期收益率复利生成财富指数
nav = pd.DataFrame({'日期': compounded_wealth.index, '净值': compounded_wealth.to_numpy()})  # 组装复合净值时间序列

# 计算历史最高点与回撤
nav['历史最高'] = nav['净值'].cummax()
nav['回撤'] = (nav['净值'] - nav['历史最高']) / nav['历史最高']

max_drawdown = nav['回撤'].min()
drawdown_check = nav.nsmallest(3, '回撤')[['日期', '净值', '历史最高', '回撤']]  # 选取回撤最深的三个观测用于核验
assert (nav['净值'] > 0).all()  # 验证复合财富路径始终为正
assert nav['回撤'].between(-1, 0).all()  # 验证回撤位于经济上有效的负一到零区间
assert np.isclose(max_drawdown, nav['回撤'].min())  # 验证最大回撤等于逐期回撤最小值
print(f'最大回撤: {max_drawdown:.2%}')  # 输出核心风险指标
print(drawdown_check)  # 输出最大回撤附近的任务特异依据
最大回撤: -25.51%
           日期        净值      历史最高        回撤
52 2024-02-22  0.821796  1.103183 -0.255068
63 2024-03-04  0.825818  1.103183 -0.251423
49 2024-02-19  0.831624  1.103183 -0.246160

净值与回撤可视化(课堂模拟)

展开净值与回撤绘图代码
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']
plt.rcParams['axes.unicode_minus'] = False

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 4.5), sharex=True)  # 使用紧凑双面板画布以适配课堂投影

ax1.plot(nav['日期'], nav['净值'], label='净值', linewidth=2)
ax1.plot(nav['日期'], nav['历史最高'], label='历史最高', linewidth=2, linestyle='--')
ax1.set_ylabel('净值')
ax1.set_title('净值曲线与回撤分析')
ax1.legend()
ax1.grid(True, alpha=0.3)

ax2.fill_between(nav['日期'], nav['回撤'], 0, alpha=0.3, color='red')
ax2.plot(nav['日期'], nav['回撤'], color='red', linewidth=2)
ax2.set_ylabel('回撤率')
ax2.set_xlabel('日期')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
图中展示课堂模拟复合净值曲线与回撤分析;读者应依据坐标、图例与注释比较主要模式。
Figure 1: 课堂模拟复合净值曲线与回撤分析

滚动窗口统计:数学原理

滚动窗口 (Rolling Window):对时间序列 \(\{x_t\}_{t=1}^T\),窗口大小为 \(w\)

\[ \large \text{RollingMean}_t = \frac{1}{w}\sum_{i=t-w+1}^t x_i \]

核心思想:只用最近 \(w\) 个数据点计算统计量,捕捉短期动态变化

滚动窗口计算实例(课堂模拟)

Listing 10: 滚动窗口统计量计算
展开滚动窗口代码
dates = pd.date_range('2024-01-01', periods=100)
prices_ts = pd.DataFrame({
    '日期': dates,
    '收盘价': 100 + np.cumsum(np.random.normal(0.5, 2, 100))
})
prices_ts = prices_ts.set_index('日期')

# 计算滚动统计量
prices_ts['MA5'] = prices_ts['收盘价'].rolling(window=5).mean()
prices_ts['MA20'] = prices_ts['收盘价'].rolling(window=20).mean()
prices_ts['STD5'] = prices_ts['收盘价'].rolling(window=5).std()

print('滚动统计量(最后10行):')
print(prices_ts.tail(10))
滚动统计量(最后10行):
                   收盘价         MA5        MA20      STD5
日期                                                      
2024-03-31  152.824201  155.063008  149.335599  1.919111
2024-04-01  155.036999  154.753174  150.176524  1.727180
2024-04-02  155.965186  154.558108  151.046569  1.451454
2024-04-03  153.973709  154.203465  151.757924  1.296751
2024-04-04  154.820070  154.524033  152.458929  1.185325
2024-04-05  156.090705  155.177334  153.115747  0.873356
2024-04-06  154.822990  155.134532  153.682879  0.887089
2024-04-07  155.630441  155.067583  154.120030  0.818724
2024-04-08  156.246858  155.522213  154.589468  0.678587
2024-04-09  154.460917  155.450382  154.672592  0.782539

布林带:基于滚动窗口的技术指标(课堂模拟)

布林带 (Bollinger Bands) 由三条线组成:

  • 中轨\(MA_{20}\)(20日移动平均)
  • 上轨\(MA_{20} + 2\sigma\)
  • 下轨\(MA_{20} - 2\sigma\)

边界触碰(未验证信号):价格触及上轨或下轨只说明相对滚动均值偏离较大;是否具有预测能力必须另做样本外回测,不能直接解释为超买、超卖或买卖建议。

布林带:末期指标核对

Listing 11: 计算布林带
展开布林带代码
prices_ts['布林带_上'] = prices_ts['MA20'] + 2 * prices_ts['收盘价'].rolling(window=20).std()
prices_ts['布林带_下'] = prices_ts['MA20'] - 2 * prices_ts['收盘价'].rolling(window=20).std()

bollinger_check = prices_ts[['收盘价', 'MA20', '布林带_上', '布林带_下']].tail(2)  # 选取末两期形成布林带核验摘要
print('布林带末两期:')  # 标明摘要保留最新技术指标状态
print(bollinger_check.round(4))  # 输出收盘价与上下轨的任务特异结果
布林带末两期:
                 收盘价      MA20     布林带_上     布林带_下
日期                                                
2024-04-08  156.2469  154.5895  157.5607  151.6183
2024-04-09  154.4609  154.6726  157.5234  151.8218

expanding 窗口:累积统计(课堂模拟)

Listing 12: 扩展窗口统计量
展开扩展窗口代码
prices_ts['累积均值'] = prices_ts['收盘价'].expanding().mean()
prices_ts['累积标准差'] = prices_ts['收盘价'].expanding().std()
prices_ts['累积最大值'] = prices_ts['收盘价'].expanding().max()

# 年化累计波动率
prices_ts['累计波动率'] = prices_ts['收盘价'].pct_change().expanding().std() * np.sqrt(252)  # 计算随样本扩展更新的年化波动率
expanding_check = prices_ts[['收盘价', '累积均值', '累积标准差', '累积最大值', '累计波动率']].tail(3)  # 保留末三期形成便于核对摘要
print('扩展窗口末三期统计:')  # 标明摘要的时间范围
print(expanding_check.round(4))  # 输出长期统计量的最新水平与变化
扩展窗口末三期统计:
                 收盘价      累积均值    累积标准差     累积最大值   累计波动率
日期                                                       
2024-04-07  155.6304  126.5879  18.2596  156.9405  0.2493
2024-04-08  156.2469  126.8875  18.4091  156.9405  0.2480
2024-04-09  154.4609  127.1632  18.5223  156.9405  0.2481

rolling vs expanding 对比

特性 rolling expanding
窗口大小 固定(如5日、20日) 不断增长(从起点到当前)
计算范围 \([t-w+1, \; t]\) \([1, \; t]\)
典型应用 移动平均、短期波动 累计收益、长期风险

分组统计 groupby:核心思想

分组聚合 (GroupBy Aggregation) 的三个步骤:

  1. Split(拆分):按键值将数据分成若干组
  2. Apply(应用):对每组数据应用聚合函数
  3. Combine(合并):将结果合并为一个新的数据结构

\[ \large \text{GroupBy}(K, f, X) = \{(k, f(\{x \mid key(x) = k\})) \mid k \in K\} \]

groupby 基础操作(课堂模拟)

数据声明:公司代码、名称与估值指标均为课堂构造,不对应任何真实公司或实时市场数据。

Listing 13: groupby基础操作
展开课堂分组数据代码
industry_data = pd.DataFrame({  # 构造匿名课堂行业样本
    '股票代码': ['SIM-A1', 'SIM-A2', 'SIM-B1', 'SIM-C1', 'SIM-B2', 'SIM-B3'],  # 设置不映射真实公司的模拟代码
    '股票名称': ['模拟白酒A', '模拟白酒B', '模拟银行A', '模拟保险A', '模拟银行B', '模拟银行C'],  # 设置匿名模拟名称
    '行业': ['白酒', '白酒', '银行', '保险', '银行', '银行'],  # 设置课堂行业标签
    '市盈率': [45.2, 35.8, 8.5, 12.3, 9.2, 6.8],  # 设置课堂模拟市盈率
    '市净率': [12.3, 8.9, 0.9, 1.5, 1.1, 0.7],  # 设置课堂模拟市净率
    'ROE': [0.28, 0.22, 0.15, 0.18, 0.13, 0.14],  # 设置课堂模拟ROE
    '股息率': [0.012, 0.018, 0.035, 0.028, 0.040, 0.045]  # 设置课堂模拟股息率
})

# 按行业分组计算均值
industry_mean = industry_data.groupby('行业').mean(numeric_only=True)
print('课堂模拟行业均值:')  # 标明输出不是实时行业统计
print(industry_mean)  # 输出匿名课堂样本的行业聚合结果
课堂模拟行业均值:
          市盈率   市净率   ROE    股息率
行业                              
保险  12.300000   1.5  0.18  0.028
白酒  40.500000  10.6  0.25  0.015
银行   8.166667   0.9  0.14  0.040

groupby 多统计量聚合(课堂模拟)

Listing 14: agg方法进行多统计量聚合
展开多统计量聚合代码
industry_stats = industry_data.groupby('行业').agg({
    '市盈率': ['mean', 'median', 'std'],
    '市净率': ['mean', 'min', 'max'],
    'ROE': 'mean',
    '股息率': 'mean'
})
print('行业详细统计:')
print(industry_stats.round(4))

# 分组计数
print('\n行业股票数量:')
print(industry_data.groupby('行业').size())
行业详细统计:
        市盈率                  市净率              ROE    股息率
       mean median     std  mean  min   max  mean   mean
行业                                                      
保险  12.3000   12.3     NaN   1.5  1.5   1.5  0.18  0.028
白酒  40.5000   40.5  6.6468  10.6  8.9  12.3  0.25  0.015
银行   8.1667    8.5  1.2342   0.9  0.7   1.1  0.14  0.040

行业股票数量:
行业
保险    1
白酒    2
银行    3
dtype: int64

多级分组统计(课堂模拟)

Listing 15: 多级分组统计
展开多级分组代码
multi_level_data = pd.DataFrame({
    '行业': ['白酒', '白酒', '白酒', '银行', '银行', '银行', '保险', '保险'],
    '市值等级': ['大盘', '中盘', '大盘', '大盘', '小盘', '大盘', '大盘', '中盘'],
    '市盈率': [45.2, 35.8, 38.5, 8.5, 15.2, 6.8, 12.3, 18.5],
    '收益率': [0.05, 0.03, 0.06, 0.02, 0.04, 0.01, 0.03, 0.02]
})

df_multi = pd.DataFrame(multi_level_data)

# 多级分组
multi_stats = df_multi.groupby(['行业', '市值等级']).agg({
    '市盈率': 'mean',
    '收益率': 'mean'
})
print('多级分组统计:')
print(multi_stats)
多级分组统计:
           市盈率    收益率
行业 市值等级              
保险 中盘    18.50  0.020
   大盘    12.30  0.030
白酒 中盘    35.80  0.030
   大盘    41.85  0.055
银行 大盘     7.65  0.015
   小盘    15.20  0.040

groupby 行业内排名

Listing 16: 按行业进行组内排名
df_multi['行业内PE排名'] = df_multi.groupby('行业')['市盈率'].rank()
print('行业内PE排名:')
print(df_multi)
行业内PE排名:
   行业 市值等级   市盈率   收益率  行业内PE排名
0  白酒   大盘  45.2  0.05      3.0
1  白酒   中盘  35.8  0.03      1.0
2  白酒   大盘  38.5  0.06      2.0
3  银行   大盘   8.5  0.02      2.0
4  银行   小盘  15.2  0.04      3.0
5  银行   大盘   6.8  0.01      1.0
6  保险   大盘  12.3  0.03      1.0
7  保险   中盘  18.5  0.02      2.0

自定义聚合函数(课堂模拟)

Listing 17: 自定义聚合函数
展开自定义聚合代码
# 自定义:基于ROE计算课堂标准化分数,不称为夏普比率
def roe_classroom_standardization(series, benchmark_roe=0.10):  # 定义课堂ROE标准化函数
    '''计算ROE均值相对课堂基准的组内标准化幅度'''  # 说明该量不是收益风险比率
    group_std = series.std()  # 计算组内ROE样本标准差
    return (series.mean() - benchmark_roe) / group_std if group_std > 0 else 0  # 标准化均值与课堂基准的距离

industry_groups = industry_data.groupby('行业')  # 建立课堂行业分组对象
pb_pe_mean = (industry_data['市净率'] / industry_data['市盈率']).groupby(industry_data['行业']).mean()  # 计算各行业市净率市盈率比率均值
roe_classroom_score = industry_groups['ROE'].apply(roe_classroom_standardization)  # 计算ROE课堂标准化分数
pe_range = industry_groups['市盈率'].agg(lambda values: values.max() - values.min())  # 计算各行业市盈率范围
custom_summary = pd.DataFrame({'市净率/市盈率均值': pb_pe_mean, 'ROE课堂标准化分数': roe_classroom_score, '市盈率范围': pe_range})  # 合并三项课堂聚合结果
print(custom_summary.round(4))  # 输出匿名课堂行业的自定义统计摘要
    市净率/市盈率均值  ROE课堂标准化分数  市盈率范围
行业                              
保险     0.1220      0.0000    0.0
白酒     0.2604      3.5355    9.4
银行     0.1095      4.0000    2.4

口径说明:ROE 课堂标准化分数是“组均值减课堂基准”除以组内样本标准差;它没有收益时间序列或无风险利率,因此不是夏普比率。

协方差与相关系数

协方差 (Covariance):

\[ \large \text{Cov}(X, Y) = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y}) \]

相关系数 (Correlation Coefficient):

\[ \large \rho_{X,Y} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} \]

  • \(\rho \approx 1\):强正相关(同涨同跌)
  • \(\rho \approx 0\):线性相关接近零,但不代表统计独立
  • \(\rho \approx -1\):强负相关(此消彼长)

相关性矩阵计算

Listing 18: 相关性矩阵计算
展开实现代码
# 使用之前的课堂模拟收益率数据
cov_matrix = df_returns.cov()
print('协方差矩阵:')
print(cov_matrix.round(6))

corr_matrix = df_returns.corr()
print('\n相关系数矩阵:')
print(corr_matrix.round(4))
协方差矩阵:
          模拟资产A     模拟资产B     模拟资产C     模拟资产D
模拟资产A  0.000330 -0.000059  0.000056 -0.000049
模拟资产B -0.000059  0.000568 -0.000014 -0.000007
模拟资产C  0.000056 -0.000014  0.000265 -0.000000
模拟资产D -0.000049 -0.000007 -0.000000  0.000253

相关系数矩阵:
        模拟资产A   模拟资产B   模拟资产C   模拟资产D
模拟资产A  1.0000 -0.1364  0.1908 -0.1702
模拟资产B -0.1364  1.0000 -0.0366 -0.0176
模拟资产C  0.1908 -0.0366  1.0000 -0.0003
模拟资产D -0.1702 -0.0176 -0.0003  1.0000

区分最大带符号相关与绝对值最强相关

Listing 19: 分别找出最大带符号相关与绝对值最强的唯一模拟资产对
展开唯一相关对代码
upper_mask = np.triu(np.ones(corr_matrix.shape, dtype=bool), k=1)  # 仅保留相关矩阵上三角以排除自相关和镜像重复
unique_pairs = corr_matrix.where(upper_mask).stack().rename('相关系数').reset_index()  # 展开唯一跨资产相关对且不按数值删除完全相关
unique_pairs.columns = ['资产1', '资产2', '相关系数']  # 设置便于业务阅读的字段名
unique_pairs['绝对相关系数'] = unique_pairs['相关系数'].abs()  # 计算排序所需的绝对相关强度
signed_top = unique_pairs.loc[unique_pairs['相关系数'].idxmax()]  # 用原值选择最大带符号相关的唯一资产对
absolute_top = unique_pairs.loc[unique_pairs['相关系数'].abs().idxmax()]  # 用绝对值选择方向不限的最强相关唯一资产对
expected_pair_count = corr_matrix.shape[0] * (corr_matrix.shape[0] - 1) // 2  # 计算无序跨资产对的理论数量
assert len(unique_pairs) == expected_pair_count  # 验证每个无序资产对只保留一次
assert not unique_pairs[['资产1', '资产2']].duplicated().any()  # 验证结果不存在镜像或重复资产对
perfect_probe = pd.DataFrame({'模拟资产甲': [1, 2, 3], '模拟资产乙': [2, 4, 6]}).corr()  # 构造跨资产完全相关的课堂边界探针
perfect_probe_mask = np.triu(np.ones(perfect_probe.shape, dtype=bool), k=1)  # 为边界探针保留唯一上三角资产对
perfect_probe_pairs = perfect_probe.where(perfect_probe_mask).stack()  # 提取跨资产完全相关值而不与对角线混淆
assert np.isclose(perfect_probe_pairs.iloc[0], 1.0)  # 验证算法保留真实跨资产完全相关系数
print('最大带符号相关唯一对:',signed_top[['资产1','资产2','相关系数']].to_dict())  # 输出原值 idxmax 的带符号口径
print('绝对值最强相关唯一对:',absolute_top[['资产1','资产2','相关系数']].to_dict())  # 输出绝对值口径并保留符号
最大带符号相关唯一对: {'资产1': '模拟资产A', '资产2': '模拟资产C', '相关系数': 0.1908399133612677}
绝对值最强相关唯一对: {'资产1': '模拟资产A', '资产2': '模拟资产C', '相关系数': 0.1908399133612677}

相关性在投资组合中的应用

现代投资组合理论 (MPT):

\[ \large \sigma_p^2 = \sum_{i=1}^n \sum_{j=1}^n w_i w_j \sigma_i \sigma_j \rho_{ij} \]

相关系数 投资含义 策略建议
\(\rho \approx 1\) 同涨同跌 分散化效果差
\(\rho \approx 0\) 线性联动弱,不等于独立 需结合非线性依赖与情景压力测试
\(\rho \approx -1\) 此消彼长 天然对冲工具

在给定权重与边际方差下,资产并非完全正相关通常提供分散化空间;实际组合风险仍取决于完整协方差矩阵、权重和样本稳定性。

本章小结

模块 核心方法 典型应用
描述性统计 mean(), std(), describe() 收益率分析
分位数与异常值 quantile(), IQR 规则 风险监控
偏度与峰度 skew(), kurtosis() 分布特征判断
累积统计 diff().cumsum(), (1+r).cumprod(), cummax() 变化重构、复合财富与回撤分析
滚动窗口 rolling(), expanding() 移动平均、布林带
分组统计 groupby(), agg() 行业对比分析
相关性分析 corr(), cov() 投资组合优化

随堂练习

  • 问题 1|需要准备哪些数据?:中性净值、收益率与行业分组教学示例。
  • 问题 2|需要完成哪些操作?:运行 lst-drawdownlst-rolling-statslst-correlation,计算回撤、滚动统计和关系矩阵。
  • 问题 3|应得到哪些结果?:最大回撤表、滚动窗口尾值、最大带符号相关唯一对与绝对值最强相关唯一对。
  • 问题 4|怎样确认结果可靠?:手算峰谷回撤,排除对角/重复对,并分别用原值 idxmax() 与绝对值 idxmax() 核对;口径混写时先检查原因。
  • 问题 5|换一个情境,怎样继续应用?:把同一组统计方法应用到本地真实净值,并说明数据来源。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 所用数据与字段:按日期升序的小数收益率;无缺失、无重复日期。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
import numpy as np  # 导入数值计算库以检查有限值
import pandas as pd  # 导入表格工具以构造、对齐和核对数据
returns=pd.DataFrame({'资产甲':[.03,-.02,.01,.04,-.03,.02,.01,-.01],'资产乙':[.02,-.01,.00,.03,-.02,.01,.02,-.01],'资产丙':[-.01,.02,.01,-.02,.03,.00,-.01,.02]},index=pd.date_range('2025-01-03',periods=8,freq='W-FRI'))  # 建立同频中性收益示例数据
assert returns.index.is_monotonic_increasing and returns.index.is_unique and np.isfinite(returns).all().all()  # 执行日期与有限值输入要求
wealth=(1+returns).cumprod()  # 计算各资产复合净值
drawdown=wealth/wealth.cummax()-1  # 计算各资产回撤路径
maximum_drawdown=drawdown.min()  # 提取各资产最大回撤
rolling_mean=returns.rolling(4,min_periods=4).mean().iloc[-1]  # 提取四期滚动均值尾值
correlation=returns.corr()  # 计算完整关系矩阵
upper=np.triu(np.ones(correlation.shape,dtype=bool),k=1)  # 构造排除对角与镜像的上三角掩码
unique_pairs=correlation.where(upper).stack().rename('correlation')  # 生成保留符号的唯一资产对序列
signed_pair=unique_pairs.loc[[unique_pairs.idxmax()]]  # 用原值idxmax选择最大带符号相关唯一对
absolute_pair=unique_pairs.loc[[unique_pairs.abs().idxmax()]]  # 用绝对值idxmax选择方向不限的最强唯一对
asset='资产甲'  # 指定另行核对资产
trough=drawdown[asset].idxmin()  # 定位该资产最大回撤谷值日
peak=wealth.loc[:trough,asset].idxmax()  # 定位谷值日前历史峰值日
assert np.isclose(maximum_drawdown[asset],wealth.loc[trough,asset]/wealth.loc[peak,asset]-1)  # 手算核对峰谷回撤
assert len(unique_pairs)==3 and not any(left==right for left,right in unique_pairs.index)  # 检查三资产仅有三个唯一资产对
assert signed_pair.index[0]==('资产甲','资产乙') and np.isclose(signed_pair.iloc[0],.938979,atol=1e-6)  # 检查示例数据最大带符号相关为+0.938979
assert absolute_pair.index[0]==('资产乙','资产丙') and np.isclose(absolute_pair.iloc[0],-1.0)  # 检查示例数据绝对值最强相关为-1
print({'最大回撤':maximum_drawdown.round(4).to_dict(),'滚动均值尾值':rolling_mean.round(4).to_dict(),'最大带符号相关唯一对':signed_pair.round(6).to_dict(),'绝对值最强相关唯一对':absolute_pair.round(6).to_dict()})  # 输出四类统计结果

教师参考解答|答案与说明 2

  • 解释答案:累计净值、运行峰值和回撤必须按时间顺序计算;滚动尾值必须标明窗口;相关输出必须排除对角与镜像重复且不解释为因果。
  • 拓展应用答案|本地真实中国基金净值

教师参考解答|代码 2

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具以执行数据来源要求
import numpy as np  # 导入数值计算库以执行有限值检查
import pandas as pd  # 导入表格工具读取本地基金行情
source=Path('/home/ubuntu/r2_data_mount/data/fund/funds.h5')  # 锁定本地真实基金数据来源
symbols=['510300.XSHG','510500.XSHG','159915.XSHE']  # 定义三只中国ETF新案例对象
required={'symbol','datetime','close'}  # 定义最小字段要求
assert source.exists()  # 来源缺失时应停止并检查数据
raw=pd.read_hdf(source,key='funds')  # 读取本地真实基金行情快照
assert required.issubset(raw.columns)  # 字段结构缺失时应停止并检查数据
raw=raw.loc[raw['symbol'].isin(symbols),['symbol','datetime','close']].copy()  # 仅保留新案例资产与必要字段
raw['date']=pd.to_datetime(raw['datetime'].astype(str),format='%Y%m%d%H%M%S',errors='coerce')  # 把数据日期规范为时间戳
raw['close']=pd.to_numeric(raw['close'],errors='coerce')  # 把收盘净值规范为数值
assert set(raw['symbol'])==set(symbols) and raw['date'].notna().all() and np.isfinite(raw['close']).all() and (raw['close']>0).all()  # 资产、日期、有限值或正值失败时请检查数据
nav=raw.pivot_table(index='date',columns='symbol',values='close',aggfunc='last').sort_index().loc['2023-01-01':'2025-12-31'].dropna()  # 对齐真实净值期间
assert nav.index.is_unique and nav.index.is_monotonic_increasing and len(nav)>=60  # 日期重复、乱序或窗口不足时请检查数据
fund_returns=nav.pct_change(fill_method=None).dropna()  # 计算同频基金收益
fund_drawdown=(nav/nav.cummax()-1).min()  # 计算各基金最大回撤
fund_rolling=fund_returns.rolling(20,min_periods=20).mean().iloc[-1]  # 计算20交易日滚动收益尾值
fund_corr=fund_returns.corr()  # 计算真实基金收益关系矩阵
fund_pairs=fund_corr.where(np.triu(np.ones(fund_corr.shape,dtype=bool),1)).stack()  # 排除对角和镜像重复资产对
fund_signed=fund_pairs.loc[[fund_pairs.idxmax()]]  # 选择真实新案例中的最大带符号相关唯一对
fund_absolute=fund_pairs.loc[[fund_pairs.abs().idxmax()]]  # 选择真实新案例中的绝对值最强相关唯一对
assert len(fund_pairs)==3 and np.isfinite(fund_drawdown).all() and np.isfinite(fund_rolling).all()  # 检查统计结果完整且有限
assert fund_signed.iloc[0]==fund_pairs.max() and abs(fund_absolute.iloc[0])==fund_pairs.abs().max()  # 另行核对两种排序口径
print({'来源':str(source),'期间':[nav.index.min().date(),nav.index.max().date()],'最大回撤':fund_drawdown.round(4).to_dict(),'滚动尾值':fund_rolling.round(6).to_dict(),'最大带符号相关唯一对':fund_signed.round(4).to_dict(),'绝对值最强相关唯一对':fund_absolute.round(4).to_dict()})  # 输出来源绑定新案例结果

教师参考解答|答案与说明 3

  • 参考结果:最大回撤、滚动窗口尾值、最大带符号相关唯一对与绝对值最强相关唯一对;拓展应用输出另含来源与实际期间。另行核对:示例数据中分别选出+0.938979与-1,并检查三资产恰有三个唯一对。
  • 常见错误:把“最大带符号相关”按绝对值排序;输出绝对强度却丢掉负号;保留自相关或镜像对。